{***************************************************************************}
{                                                                           }
{           Spring Framework for Delphi                                     }
{                                                                           }
{           Copyright (c) 2009-2013 Spring4D Team                           }
{                                                                           }
{           http://www.spring4d.org                                         }
{                                                                           }
{***************************************************************************}
{                                                                           }
{  Licensed under the Apache License, Version 2.0 (the "License");          }
{  you may not use this file except in compliance with the License.         }
{  You may obtain a copy of the License at                                  }
{                                                                           }
{      http://www.apache.org/licenses/LICENSE-2.0                           }
{                                                                           }
{  Unless required by applicable law or agreed to in writing, software      }
{  distributed under the License is distributed on an "AS IS" BASIS,        }
{  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. }
{  See the License for the specific language governing permissions and      }
{  limitations under the License.                                           }
{                                                                           }
{***************************************************************************}

unit Spring.Tests.Collections.Enumerable;

//{$I Spring.inc}

interface

uses
  TestFramework;

type
  TTestEnumerable = class(TTestCase)
  published
    procedure TestAggregate;
    procedure TestAggregate2;
    procedure TestDistinct;
    procedure TestExcept;
    procedure TestIntersect;
    procedure TestUnion;
    procedure TestSelect;
    procedure TestSelectIndex;
    procedure TestToLookup;
    procedure TestGroupBy;
    procedure TestAverage;
    procedure TestJoin;
    procedure TestGroupJoin;
    procedure TestSelectMany;
    procedure TestOrderBy;
    procedure TestThenBy;
    procedure TestZip;
  end;

implementation

uses
  StrUtils,
  SysUtils,
  Spring.Collections,
  Spring.Collections.Enumerable;

const
  FuzzFactor = 1000;
  ExtendedResolution = 1E-19 * FuzzFactor;
  DoubleResolution   = 1E-15 * FuzzFactor;
  SingleResolution   = 1E-7 * FuzzFactor;

{ TTestEnumerable }

procedure TTestEnumerable.TestAggregate;
var
  words: IList<string>;
  reversed: string;
begin
  words := TCollections.CreateList<string>;
  words.AddRange(['the', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog']);
  reversed := Enumerable<string>(words).Aggregate(
    function(workingSentence, next: string): string
    begin
      Result := next + ' ' + workingSentence;
    end);
  CheckEquals('dog lazy the over jumps fox brown quick the', reversed);
end;

procedure TTestEnumerable.TestAggregate2;
var
  fruits: IList<string>;
  longestName: string;
begin
  fruits := TCollections.CreateList<string>;
  fruits.AddRange(['apple', 'mango', 'orange', 'passionfruit', 'grape']);

  longestName :=
    Enumerable<string>(fruits).Aggregate<string, string>('banana',
      function(longest, next: string): string
      begin
        Result := IfThen(Length(next) > Length(longest), next, longest);
      end,
      function(fruit: string): string
      begin
        Result := UpperCase(fruit);
      end);

  CheckEquals('PASSIONFRUIT', longestName);
end;

procedure TTestEnumerable.TestAverage;
var
  numbers: ICollection<string>;
  average: Double;
begin
  numbers := TCollections.CreateList<string>;
  numbers.AddRange(['10007', '37', '299846234235']);

  average := Enumerable<string>(numbers).Average(
    function(num: string): Int64
    begin
      Result := StrToInt64(num);
    end);

  CheckEquals(99948748093, average);
end;

procedure TTestEnumerable.TestDistinct;
var
  values: IList<Integer>;
  distinctValues: TArray<Integer>;
begin
  values := TCollections.CreateList<Integer>;
  values.AddRange([21, 46, 46, 55, 17, 21, 55, 55]);
  distinctValues := Enumerable<Integer>(values).Distinct.ToArray;

  CheckEquals(4, Length(distinctValues));
  CheckEquals(21, distinctValues[0]);
  CheckEquals(46, distinctValues[1]);
  CheckEquals(55, distinctValues[2]);
  CheckEquals(17, distinctValues[3]);
end;

procedure TTestEnumerable.TestExcept;
var
  numbers1, numbers2: ICollection<Double>;
  onlyInFirstSet: TArray<Double>;
begin
  numbers1 := TCollections.CreateList<Double>;
  numbers1.AddRange([2.0, 2.1, 2.2, 2.3, 2.4, 2.5]);
  numbers2 := TCollections.CreateList<Double>;
  numbers2.Add(2.2);
  onlyInFirstSet := Enumerable<Double>(numbers1).ExceptWith(numbers2);

  CheckEquals(5, Length(onlyInFirstSet));
  CheckEquals(2.0, onlyInFirstSet[0], DoubleResolution);
  CheckEquals(2.1, onlyInFirstSet[1], DoubleResolution);
  CheckEquals(2.3, onlyInFirstSet[2], DoubleResolution);
  CheckEquals(2.4, onlyInFirstSet[3], DoubleResolution);
  CheckEquals(2.5, onlyInFirstSet[4], DoubleResolution);
end;

type
  TPerson = class
  private
    fName: string;
  public
    constructor Create(const Name: string);
    property Name: string read fName;
  end;

  TPet = record
    Name: string;
    Age: Integer;
    Owner: TPerson;
    constructor Create(const Name: string; Owner: TPerson); overload;
    constructor Create(const Name: string; Age: Integer); overload;
  end;

constructor TPerson.Create(const Name: string);
begin
  fName := Name;
end;

constructor TPet.Create(const Name: string; Owner: TPerson);
begin
  Self.Name := Name;
  Self.Owner := Owner;
end;

constructor TPet.Create(const Name: string; Age: Integer);
begin
  Self.Name := Name;
  Self.Age := Age;
end;

procedure TTestEnumerable.TestGroupBy;
const
  pets: array[0..3] of TPet = (
    (Name: 'Barley'; Age: 8),
    (Name: 'Boots'; Age: 4),
    (Name: 'Whiskers'; Age: 1),
    (Name: 'Daisy'; Age: 4)
  );
  ages: array[0..2] of Integer = (8, 4, 1);
  names: array[0..3] of string = ('Barley', 'Boots', 'Daisy', 'Whiskers');
var
  petsList: IList<TPet>;
  query: IEnumerable<IGrouping<Integer, string>>;
  petGroup: IGrouping<Integer, string>;
  name: string;
  i, k: Integer;
begin
{$IFNDEF DELPHIXE4_UP}
  petsList := TCollections.CreateList<TPet>;
  petsList.AddRange(pets);

  query := Enumerable<TPet>(petsList).GroupBy<Integer, string>(
    function(pet: TPet): Integer
    begin
      Result := pet.Age;
    end,
    function(pet: TPet): string
    begin
      Result := pet.Name;
    end);

  i := 0;
  k := 0;
  for petGroup in query do
  begin
    CheckEquals(ages[i], petGroup.Key);
    Inc(i);
    for name in petGroup do
    begin
      CheckEquals(name, names[k]);
      Inc(k);
    end;
  end;
{$ENDIF}
end;

type
  TPetOwner = record
    OwnerName: string;
    Pets: IEnumerable<string>;
    constructor Create(OwnerName: string; Pets: IEnumerable<string>);
  end;

constructor TPetOwner.Create(OwnerName: string; Pets: IEnumerable<string>);
begin
  Self.OwnerName := OwnerName;
  Self.Pets := Pets;
end;

procedure TTestEnumerable.TestGroupJoin;
const
  values: array[0..3] of string = (
    'Daisy', 'Barley', 'Boots', 'Whiskers');
var
  magnus, terry, charlotte: TPerson;
  barley, boots, whiskers, daisy: TPet;
  people: IList<TPerson>;
  pets: IList<TPet>;
  query: IEnumerable<TPetOwner>;
  obj: TPetOwner;
  pet: string;
  i, k: Integer;
begin
  magnus := TPerson.Create('Hedlund, Magnus');
  terry := TPerson.Create('Adams, Terry');
  charlotte := TPerson.Create('Weiss, Charlotte');

  barley := TPet.Create('Barley', terry);
  boots := TPet.Create('Boots', terry);
  whiskers := TPet.Create('Whiskers', charlotte);
  daisy := TPet.Create('Daisy', magnus);

  people := TCollections.CreateObjectList<TPerson>;
  people.AddRange([magnus, terry, charlotte]);
  pets := TCollections.CreateList<TPet>;
  pets.AddRange([barley, boots, whiskers, daisy]);

  query := Enumerable<TPerson>(people).GroupJoin<TPet, TPerson, TPetOwner>(pets,
    function(person: TPerson): TPerson
    begin
      Result := person;
    end,
    function(pet: TPet): TPerson
    begin
      Result := pet.Owner
    end,
    function(person: TPerson; petCollection: IEnumerable<TPet>): TPetOwner
    begin
      Result.OwnerName := person.Name;
      Result.Pets := Enumerable<TPet>(petCollection).Select<string>(
        function(pet: TPet): string
        begin
          Result := pet.Name;
        end);
    end);

  i := 0;
  k := 0;
  for obj in query do
  begin
    CheckEquals(people[i].Name, obj.OwnerName);
    Inc(i);
    for pet in obj.Pets do
    begin
      CheckEquals(values[k], pet);
      Inc(k);
    end;
  end;
end;

procedure TTestEnumerable.TestIntersect;
var
  id1, id2: ICollection<Byte>;
  both: TArray<Byte>;
begin
  id1 := TCollections.CreateList<Byte>;
  id1.AddRange([44, 26, 92, 30, 71, 38]);
  id2 := TCollections.CreateList<Byte>;
  id2.AddRange([39, 59, 83, 47, 26, 4, 30]);
  both := Enumerable<Byte>(id1).IntersectWith(id2);

  CheckEquals(2, Length(both));
  CheckEquals(26, both[0]);
  CheckEquals(30, both[1]);
end;

type
  TPair = record
    OwnerName: string;
    Pet: string;
  end;

procedure TTestEnumerable.TestJoin;
const
  values: array[0..3] of string = (
    'Hedlund, Magnus - Daisy',
    'Adams, Terry - Barley',
    'Adams, Terry - Boots',
    'Weiss, Charlotte - Whiskers');
var
  magnus, terry, charlotte: TPerson;
  barley, boots, whiskers, daisy: TPet;
  people: IList<TPerson>;
  pets: IList<TPet>;
  query: IEnumerable<TPair>;
  i: Integer;
  obj: TPair;
begin
  magnus := TPerson.Create('Hedlund, Magnus');
  terry := TPerson.Create('Adams, Terry');
  charlotte := TPerson.Create('Weiss, Charlotte');

  barley := TPet.Create('Barley', terry);
  boots := TPet.Create('Boots', terry);
  whiskers := TPet.Create('Whiskers', charlotte);
  daisy := TPet.Create('Daisy', magnus);

  people := TCollections.CreateObjectList<TPerson>;
  people.AddRange([magnus, terry, charlotte]);
  pets := TCollections.CreateList<TPet>;
  pets.AddRange([barley, boots, whiskers, daisy]);

  query := Enumerable<TPerson>(people).Join<TPet, TPerson, TPair>(pets,
    function(person: TPerson): TPerson
    begin
      Result := person;
    end,
    function(pet: TPet): TPerson
    begin
      Result := pet.Owner
    end,
    function(person: TPerson; pet: TPet): TPair
    begin
      Result.OwnerName := person.Name;
      Result.Pet := pet.Name;
    end);

  i := 0;
  for obj in query do
  begin
    CheckEquals(values[i], obj.OwnerName + ' - ' + obj.Pet);
    Inc(i);
  end;
  CheckEquals(4, i);
end;

procedure TTestEnumerable.TestOrderBy;
const
  values: array[0..2] of string = (
    'Whiskers - 1',
    'Boots - 4',
    'Barley - 8');
var
  pets: ICollection<TPet>;
  query: IEnumerable<TPet>;
  i: Integer;
  pet: TPet;
begin
  pets := TCollections.CreateList<TPet>([
    TPet.Create('Barley', 8),
    TPet.Create('Boots', 4),
    TPet.Create('Whiskers', 1)]);
  query := Enumerable<TPet>(pets).OrderBy<Integer>(
    function(pet: TPet): Integer
    begin
      Result := pet.Age;
    end);

  i := 0;
  for pet in query do
  begin
    CheckEquals(values[i], pet.Name + ' - ' + IntToStr(pet.Age));
    Inc(i);
  end;
  CheckEquals(3, i);
end;

procedure TTestEnumerable.TestSelect;
var
  squares: TArray<Integer>;
  range: ICollection<Integer>;
begin
  range := TCollections.CreateList<Integer>;
  range.AddRange([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
  squares := Enumerable<Integer>(range).Select<Integer>(
    function(x: Integer): Integer
    begin
      Result := x * x;
    end);

  CheckEquals(10, Length(squares));
  CheckEquals(1, squares[0]);
  CheckEquals(4, squares[1]);
  CheckEquals(9, squares[2]);
  CheckEquals(16, squares[3]);
  CheckEquals(25, squares[4]);
  CheckEquals(36, squares[5]);
  CheckEquals(49, squares[6]);
  CheckEquals(64, squares[7]);
  CheckEquals(81, squares[8]);
  CheckEquals(100, squares[9]);
end;

type
  TFruit = record
    index: Integer;
    str: string;
  end;

procedure TTestEnumerable.TestSelectIndex;
var
  fruits: IList<string>;
  query: TArray<TFruit>;
begin
  fruits := TCollections.CreateList<string>;
  fruits.AddRange(['apple', 'banana', 'mango', 'orange', 'passionfruit', 'grape']);

  query := Enumerable<string>(fruits).Select<TFruit>(
    function(fruit: string; index: Integer): TFruit
    begin
      Result.index := index;
      Result.str := Copy(fruit, 0, index);
    end);

  CheckEquals(6, Length(query));
  CheckEquals(0, query[0].index);
  CheckEquals('', query[0].str);
  CheckEquals(1, query[1].index);
  CheckEquals('b', query[1].str);
  CheckEquals(2, query[2].index);
  CheckEquals('ma', query[2].str);
  CheckEquals(3, query[3].index);
  CheckEquals('ora', query[3].str);
  CheckEquals(4, query[4].index);
  CheckEquals('pass', query[4].str);
  CheckEquals(5, query[5].index);
  CheckEquals('grape', query[5].str);
end;

procedure TTestEnumerable.TestSelectMany;
const
  values: array[0..6] of string = (
    '0Scruffy', '0Sam', '1Walker', '1Sugar', '2Scratches', '2Diesel', '3Dusty');
var
  petOwners: ICollection<TPetOwner>;
  query: IEnumerable<string>;
  pet: string;
  i: Integer;
begin
  petOwners := TCollections.CreateList<TPetOwner>([
    TPetOwner.Create('Higa, Sidney',
      TCollections.CreateList<string>(['Scruffy', 'Sam'])),
    TPetOwner.Create('Ashkenazi, Ronen',
      TCollections.CreateList<string>(['Walker', 'Sugar'])),
    TPetOwner.Create('Price, Vernette',
      TCollections.CreateList<string>(['Scratches', 'Diesel'])),
    TPetOwner.Create('Hines, Patrick',
      TCollections.CreateList<string>(['Dusty']))]);

  query := Enumerable<TPetOwner>(petOwners).SelectMany<string>(
    function(petOwner: TPetOwner; index: Integer): IEnumerable<string>
    begin
      Result := Enumerable<string>(petOwner.Pets).Select<string>(
        function(pet: string): string
        begin
          Result := IntToStr(index) + pet;
        end);
    end);

  i := 0;
  for pet in query do
  begin
    CheckEquals(values[i], pet);
    Inc(i);
  end;
end;

type
  TPackage = class
  private
    FCompany: string;
    FWeight: Double;
    FTrackingNumber: Int64;
  public
    constructor Create(Company: string; Weight: Double; TrackingNumber: Int64);
    property Company: string read FCompany write FCompany;
    property Weight: Double read FWeight write FWeight;
    property TrackingNumber: Int64 read FTrackingNumber write FTrackingNumber;
  end;

constructor TPackage.Create(Company: string; Weight: Double;
  TrackingNumber: Int64);
begin
  FCompany := Company;
  FWeight := Weight;
  FTrackingNumber := TrackingNumber;
end;

procedure TTestEnumerable.TestThenBy;
const
  values: array[0..7] of string = (
    'apple', 'grape', 'mango', 'banana', 'orange',
    'blueberry', 'raspberry', 'passionfruit');
var
  fruits: ICollection<string>;
  query: IEnumerable<string>;
  i: Integer;
  fruit: string;
begin
  fruits := TCollections.CreateList<string>([
    'grape', 'passionfruit', 'banana', 'mango',
    'orange', 'raspberry', 'apple', 'blueberry']);
  query := Enumerable<string>(fruits).OrderBy<Integer>(
    function(fruit: string): Integer
    begin
      Result := Length(fruit);
    end).ThenBy<string>(
    function(fruit: string): string
    begin
      Result := fruit;
    end);

  i := 0;
  for fruit in query do
  begin
    CheckEquals(values[i], fruit);
    Inc(i);
  end;
  CheckEquals(8, i);
end;

procedure TTestEnumerable.TestToLookup;
const
  groups: array[0..2] of Char = ('C', 'L', 'W');
  strings: array[0..4] of string = (
    'Coho Vineyard 89453312',
    'Contoso Pharmaceuticals 670053128',
    'Lucerne Publishing 89112755',
    'Wingtip Toys 299456122',
    'Wide World Importers 4665518773'
  );
var
  packages: IList<TPackage>;
  lookup: ILookup<Char, string>;
  packageGroup: IGrouping<Char, string>;
  str: string;
  i, k: Integer;
begin
  packages := TCollections.CreateList<TPackage>(True);
  packages.AddRange([
    TPackage.Create('Coho Vineyard', 25.2, 89453312),
    TPackage.Create('Lucerne Publishing', 18.7, 89112755),
    TPackage.Create('Wingtip Toys',6.0, 299456122),
    TPackage.Create('Contoso Pharmaceuticals', 9.3, 670053128),
    TPackage.Create('Wide World Importers', 33.8, 4665518773)]);

  lookup := Enumerable<TPackage>(packages).ToLookup<Char, string>(
    function(p: TPackage): Char
    begin
      Result := p.Company[1];
    end,
    function(p: TPackage): string
    begin
      Result := p.Company + ' ' + IntToStr(p.TrackingNumber);
    end);

  i := 0;
  k := 0;
  for packageGroup in lookup do
  begin
    CheckEquals(groups[i], packageGroup.Key);
    Inc(i);
    for str in packageGroup do
    begin
      CheckEquals(strings[k], str);
      Inc(k);
    end;
  end;
  CheckEquals(3, i);
end;

procedure TTestEnumerable.TestUnion;
var
  ints1, ints2: ICollection<Byte>;
  union: TArray<Byte>;
begin
  ints1 := TCollections.CreateList<Byte>;
  ints1.AddRange([5, 3, 9, 7, 5, 9, 3, 7]);
  ints2 := TCollections.CreateList<Byte>;
  ints2.AddRange([8, 3, 6, 4, 4, 9, 1, 0]);
  union := Enumerable<Byte>(ints1).UnionWith(ints2);

  CheckEquals(9, Length(union));
  CheckEquals(5, union[0]);
  CheckEquals(3, union[1]);
  CheckEquals(9, union[2]);
  CheckEquals(7, union[3]);
  CheckEquals(8, union[4]);
  CheckEquals(6, union[5]);
  CheckEquals(4, union[6]);
  CheckEquals(1, union[7]);
  CheckEquals(0, union[8]);
end;

procedure TTestEnumerable.TestZip;
var
  numbers: ICollection<Integer>;
  words: ICollection<string>;
  numbersAndWords: TArray<string>;
begin
  numbers := TCollections.CreateList<Integer>([1, 2, 3, 4]);
  words := TCollections.CreateList<string>(['one', 'two', 'three']);

  numbersAndWords := Enumerable<Integer>(numbers).Zip<string, string>(words,
    function(first: Integer; second: string): string
    begin
      Result := IntToStr(first) + ' ' + second;
    end);

  CheckEquals(3, Length(numbersAndWords));
  CheckEquals('1 one', numbersAndWords[0]);
  CheckEquals('2 two', numbersAndWords[1]);
  CheckEquals('3 three', numbersAndWords[2]);
end;

end.
